Skip to content

Add semantic focus cropping#46

Open
TorstenDittmann wants to merge 4 commits into
mainfrom
feat/semantic-focus-crop
Open

Add semantic focus cropping#46
TorstenDittmann wants to merge 4 commits into
mainfrom
feat/semantic-focus-crop

Conversation

@TorstenDittmann

Copy link
Copy Markdown
Contributor

Summary

  • extend Image::crop() with an optional natural-language focus argument
  • use quantized OWL-ViT through TransformersPHP and ONNX Runtime to locate arbitrary subjects
  • keep all matching regions when possible and score candidate crops when subjects cannot all fit
  • retain the existing gravity as fallback when no subject matches
  • reuse one detector session per PHP worker and clean up temporary analysis images
  • install and enable FFI in the PHP test images and use reproducible Composer installs
  • document model preloading, platform setup, and the glibc requirement for prebuilt Linux ONNX Runtime artifacts

Examples

$image->crop(400, 300, focus: 'person');
$image->crop(400, 300, Image::GRAVITY_TOP, focus: 'red car');

Testing

  • ./vendor/bin/phpunit --filter '/^(?!.*(avif|heic)).*$/' (51 tests, 282 assertions)
  • focused gravity and semantic crop tests in PHP 8.3 Docker (32 tests, 155 assertions)
  • real OWL-ViT inference against the kitten fixture on macOS ARM64
  • composer lint
  • composer check
  • composer validate --strict
  • composer audit
  • PHP 8.3 Docker build and FFI execution

Known Platform Constraint

TransformersPHP publishes glibc-linked Linux ONNX Runtime binaries. The existing Alpine/musl test image can build and run the non-inference suite with FFI enabled, but real model inference requires a musl-compatible ONNX Runtime build. This limitation is documented and inference failures are not silently converted to gravity fallback.

The three excluded full-suite tests are the existing local AVIF/HEIC encoder failures caused by unavailable/incompatible ImageMagick delegates.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds semantic focus cropping to Image::crop() by integrating a quantized OWL-ViT zero-shot object detection pipeline (TransformersPHP + ONNX Runtime) via an optional focus string argument. When subjects are detected, a scoring-based algorithm selects the crop window that maximises weighted coverage of all detected regions, falling back to the supplied gravity when nothing is detected.

  • The core scoring logic in crop() is mathematically sound: bounding boxes are normalised to [0, 1], candidate crop windows are clamped to image bounds, and the intersection-over-region-area × confidence score correctly prioritises high-confidence subjects.
  • detectFocus() validates every field of every detection box (type, range, degenerate-box guard) and cleans up the temp file in a finally block — the implementation is robust against malformed pipeline output.
  • composer.json places codewithkyrian/transformers, ext-ffi, and three Symfony packages in require, so every downstream consumer that runs composer install will trigger the platform installer to download glibc-linked ONNX Runtime binaries (hundreds of MB) and will fail on environments where FFI is disabled — even when focus is never passed.

Confidence Score: 3/5

The crop logic and detection plumbing are well-implemented, but unconditionally requiring the ML runtime stack in composer.json breaks every consumer that does not need the focus feature — this warrants a rethink of the dependency structure before merging a library package.

The implementation of detectFocus() and the candidate-scoring algorithm in crop() are correct and well-tested. The concern is the composer.json change: adding codewithkyrian/transformers and ext-ffi to require forces all downstream library users to download large platform binaries and requires FFI to be compiled/enabled, even in codebases that never use the focus parameter. For a library package consumed by third parties this is a breaking install-time constraint, already flagged in a previous review round without resolution in the diff.

composer.json and composer.lock — the dependency placement (require vs suggest) and the resulting dev-branch transitive resolution of imagine/imagine, psr/log, and psr/container deserve another look before this is shipped as a library release.

Important Files Changed

Filename Overview
src/Image/Image.php Adds detectFocus() helper and extends crop() with an optional focus parameter; scoring logic for candidate crop windows is sound, static pipeline reuse is intentional, validation of detection boxes is thorough
composer.json Moves heavy ML runtime (codewithkyrian/transformers, ext-ffi, Symfony packages) into require, forcing all consumers to download platform-specific ONNX Runtime binaries and requiring FFI regardless of whether focus is used
tests/Image/ImageTest.php Adds four focused-crop tests using anonymous class overrides of detectFocus(); covers detected-region use, all-regions-fit, score-based fallback, and gravity fallback — all without requiring real model inference
Dockerfile-php-8.3 Switches to composer install (reproducible), drops --no-plugins so the platform installer runs, adds FFI extension to Alpine — correct for the test scenario; glibc ONNX Runtime limitation is documented
composer.lock Adds ~15 new runtime packages; several PSR packages (psr/log, psr/container) and imagine/imagine resolve to dev-branch versions due to minimum-stability: dev, which is pinned in the lock file but will pick up dev commits on the next composer update

Reviews (2): Last reviewed commit: "chore: ignore local focus crop examples" | Re-trigger Greptile

Comment thread src/Image/Image.php
Comment on lines +269 to +286
protected function detectFocus(string $focus): array
{
self::$focusDetector ??= pipeline('zero-shot-object-detection');

$path = tempnam(sys_get_temp_dir(), 'utopia-image-focus-');
if ($path === false) {
throw new Exception('Failed to create image for focus detection');
}

try {
if (file_put_contents($path, $this->image->getImageBlob(), LOCK_EX) === false) {
throw new Exception('Failed to create image for focus detection');
}

$detections = (self::$focusDetector)($path, [$focus], threshold: 0.1, percentage: true, topK: PHP_INT_MAX);
} finally {
@unlink($path);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Hard failure when inference runtime is unavailable

If pipeline() or the pipeline invocation throws (e.g., missing or incompatible ONNX Runtime, model not yet downloaded), the exception propagates straight through crop() and kills the entire image operation. Because the focus feature is additive and the caller already supplies a gravity fallback, users likely expect the method to fall back to gravity rather than crash. The PR notes this explicitly as a design choice, but it will cause unhandled exceptions for any caller on Alpine/musl or any environment where the binary artifacts have not been pre-downloaded, even if they are fine with a gravity-based result.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Image/Image.php
Line: 269-286

Comment:
**Hard failure when inference runtime is unavailable**

If `pipeline()` or the pipeline invocation throws (e.g., missing or incompatible ONNX Runtime, model not yet downloaded), the exception propagates straight through `crop()` and kills the entire image operation. Because the `focus` feature is additive and the caller already supplies a gravity fallback, users likely expect the method to fall back to gravity rather than crash. The PR notes this explicitly as a design choice, but it will cause unhandled exceptions for any caller on Alpine/musl or any environment where the binary artifacts have not been pre-downloaded, even if they are fine with a gravity-based result.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment thread src/Image/Image.php
{
self::$focusDetector ??= pipeline('zero-shot-object-detection');

$path = tempnam(sys_get_temp_dir(), 'utopia-image-focus-');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 tempnam prefix truncated on Windows

The prefix 'utopia-image-focus-' is 19 characters. PHP's tempnam() documentation states that on Windows only the first 3 characters of the prefix are used, so the created file gets the prefix uto instead. Keeping the prefix at 5 characters or fewer would work reliably on all platforms.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Image/Image.php
Line: 273

Comment:
**`tempnam` prefix truncated on Windows**

The prefix `'utopia-image-focus-'` is 19 characters. PHP's `tempnam()` documentation states that on Windows only the first 3 characters of the prefix are used, so the created file gets the prefix `uto` instead. Keeping the prefix at 5 characters or fewer would work reliably on all platforms.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant